Guida Completa a Scapy: Ether, ARP, hexdump e srp

Introduzione a Scapy

Che cos’è Scapy e perché è importante?

Scapy è una potentissima libreria Python che rappresenta molto più di un semplice strumento di analisi di rete. È, in sostanza, un framework completo per la manipolazione di pacchetti di rete che opera a tutti i livelli dello stack protocollare, dal livello fisico fino al livello applicativo. Per comprendere appieno il valore di Scapy, dobbiamo prima capire cosa significa “manipolare pacchetti di rete” e perché questo è così importante nel contesto dell’informatica moderna.

Quando parliamo di comunicazione di rete, ci riferiamo essenzialmente allo scambio di pacchetti - piccole unità di dati che viaggiano attraverso cavi, onde radio o fibra ottica, trasportando informazioni da un dispositivo all’altro. Ogni pacchetto è come una lettera: ha un mittente, un destinatario, un contenuto e una struttura ben definita che permette ai dispositivi di rete di instradarlo correttamente verso la sua destinazione finale.

La maggior parte degli strumenti di rete tradizionali (come Wireshark, tcpdump o anche ping) permettono di osservare questi pacchetti mentre transitano sulla rete, un po’ come se fossimo degli spettatori che guardano il traffico stradale. Scapy, invece, ci mette direttamente al volante: non solo possiamo osservare i pacchetti, ma possiamo anche costruirli da zero, modificarli, inviarli e analizzare le risposte con un controllo totale e granulare su ogni singolo bit che compone il pacchetto.

La filosofia di Scapy: potenza e semplicità

Immaginate di dover costruire una casa. Potreste usare mattoni prefabbricati di dimensioni e forme standard (approccio tradizionale delle librerie di rete), oppure potreste avere accesso a un laboratorio dove potete creare mattoni personalizzati di qualsiasi forma, dimensione e materiale desideriate (approccio di Scapy). Quest’ultima opzione vi dà una flessibilità enormemente maggiore, ma richiede anche una comprensione più profonda di come funzionano realmente le cose.

La filosofia di design di Scapy si basa su tre pilastri fondamentali:

  1. Semplicità d’uso: Nonostante la sua potenza, Scapy rende incredibilmente semplici operazioni che normalmente richiederebbero centinaia di righe di codice. Per esempio, creare e inviare un pacchetto ARP richiede appena 2-3 righe di codice Python.

  2. Flessibilità totale: Non esistono limitazioni su cosa potete fare. Volete creare un pacchetto con campi non standard? Nessun problema. Volete modificare il TTL di un pacchetto IP? Facilissimo. Volete simulare un particolare comportamento di rete per testare come reagisce il vostro firewall? Scapy è lo strumento giusto.

  3. Interattività: Scapy può essere usato sia in modalità interattiva (molto utile per sperimentare e imparare) sia all’interno di script Python complessi (perfetto per automatizzare task ripetitivi o creare strumenti personalizzati).

Perché Scapy è fondamentale per l’apprendimento?

Nel contesto didattico, Scapy rappresenta uno strumento pedagogico di valore inestimabile. Quando si studiano i protocolli di rete sui libri o nelle slide delle lezioni, si apprendono concetti teorici: “ARP risolve indirizzi IP in indirizzi MAC”, “Ethernet incapsula i pacchetti IP”, “TCP usa un three-way handshake per stabilire connessioni”. Ma spesso questi concetti rimangono astratti, difficili da visualizzare concretamente.

Con Scapy, invece, potete toccare con mano questi concetti. Potete costruire un pacchetto ARP, vederne esattamente la struttura byte per byte, inviarlo sulla rete e vedere cosa succede. Potete modificare un campo, reinviarlo e osservare come cambia il comportamento dei dispositivi di rete. Questo approccio “hands-on” trasforma l’apprendimento da passivo ad attivo, permettendo di sviluppare una comprensione profonda e intuitiva di come funzionano realmente le reti.

Ambiti di applicazione di Scapy

Le applicazioni pratiche di Scapy sono vastissime e spaziano attraverso diversi domini dell’informatica:

1. Network Analysis e Troubleshooting
Quando una rete non funziona come dovrebbe, Scapy permette di investigare a fondo. Potete inviare pacchetti specifici per testare il comportamento di router, switch o firewall, analizzando le risposte per identificare esattamente dove si trova il problema. È come avere un microscopio elettronico per le reti.

2. Security Testing e Penetration Testing
Nel campo della cybersecurity, Scapy è uno strumento standard. Permette di testare la robustezza di sistemi di rete simulando vari tipi di attacchi (sempre in modo etico e autorizzato!). Potete verificare se un firewall filtra correttamente pacchetti malformati, testare la resistenza a ARP spoofing, o verificare la corretta implementazione di protocolli di sicurezza.

3. Sviluppo e Testing di Protocolli
Se state sviluppando un nuovo protocollo di rete o implementando uno esistente, Scapy permette di creare test cases complessi. Potete simulare vari scenari di rete, generare traffico anomalo per testare la robustezza, verificare la conformità a specifiche RFC.

4. Network Discovery e Mapping
Scapy eccelle nella discovery di reti: potete scoprire quali dispositivi sono attivi, quali servizi stanno eseguendo, come sono configurati. Tutto questo con un controllo molto più fine rispetto a strumenti standard come nmap.

5. Ricerca e Sperimentazione
Nel mondo accademico e della ricerca, Scapy è ampiamente utilizzato per sperimentare con nuove idee, testare ipotesi su comportamenti di rete, raccogliere dati per analisi statistiche sul traffico di rete.

Come funziona Scapy: uno sguardo sotto il cofano

A livello tecnico, Scapy lavora direttamente con le socket raw (socket grezze) del sistema operativo. Le socket raw permettono a un programma di accedere ai protocolli di rete a un livello molto basso, bypassando gran parte dello stack di rete del sistema operativo. Questo significa che quando create un pacchetto con Scapy, state letteralmente specificando ogni singolo byte che verrà messo “sul filo” (o trasmesso via wireless).

Quando usate strumenti di rete normali (come un browser web o un client email), il sistema operativo gestisce automaticamente la creazione dei pacchetti: voi fornite i dati di alto livello ("vai su www.example.com") e il sistema operativo si occupa di tutto il resto (risoluzione DNS, creazione pacchetti TCP/IP, frammentazione, ritrasmissione in caso di errori, ecc.). Con Scapy, invece, questo controllo automatico viene meno: siete voi a decidere esattamente cosa fare.

Questo approccio ha dei pro e dei contro:

Vantaggi:

Svantaggi:

Scapy in modalità interattiva vs. scripting

Una delle caratteristiche distintive di Scapy è la sua eccellente modalità interattiva. Potete avviare Scapy dalla shell Python e iniziare immediatamente a sperimentare:

$ sudo scapy
Welcome to Scapy
>>> packet = IP(dst="192.168.1.1")/ICMP()
>>> packet.show()
>>> send(packet)

Questa modalità è fantastica per:

Tuttavia, per task più complessi o ripetitivi, è meglio scrivere script Python completi che importano Scapy come libreria. Questo approccio permette di:

In questa guida utilizzeremo principalmente l’approccio degli script Python, ma tenete presente che tutto può essere fatto anche interattivamente.

Installazione di Scapy

Prima di procedere con l’analisi delle funzioni specifiche, è importante installare correttamente Scapy:

pip install scapy

Su sistemi Linux potrebbe essere necessario eseguire gli script con privilegi di amministratore (sudo) per accedere alle interfacce di rete a basso livello.


1. Ether: Il Livello Data Link - La Fondazione della Comunicazione di Rete

Comprendere il contesto: il modello a strati delle reti

Prima di immergerci nei dettagli di Ether, è fondamentale comprendere dove si colloca questo concetto nell’architettura delle reti. Le reti informatiche sono organizzate secondo un modello a strati (o “layer”), dove ogni strato fornisce servizi specifici allo strato superiore e si basa sui servizi forniti dallo strato inferiore. I due modelli di riferimento principali sono il modello OSI (7 strati) e il modello TCP/IP (4 strati).

Modello OSI semplificato:

  1. Fisico - Trasmissione di bit sul mezzo fisico (cavi, onde radio)
  2. Data Link - Comunicazione tra dispositivi direttamente connessi (← Ether lavora qui!)
  3. Network - Routing tra reti diverse (IP)
  4. Transport - Comunicazione end-to-end affidabile (TCP/UDP)
  5. Session - Gestione delle sessioni
  6. Presentation - Formattazione dati
  7. Application - Applicazioni utente (HTTP, FTP, ecc.)

Il livello Data Link (livello 2) è responsabile del trasferimento affidabile di dati tra due nodi direttamente connessi nella stessa rete locale. “Direttamente connessi” significa che non c’è un router tra loro - potrebbero esserci switch o hub, ma questi operano proprio al livello 2 e sono quindi “trasparenti” dal punto di vista del Data Link.

Cos’è Ether in Scapy?

Ether è la classe di Scapy che rappresenta un frame Ethernet, ovvero l’unità di dati fondamentale del protocollo Ethernet che opera al livello 2 del modello OSI. Ma cosa significa esattamente “frame Ethernet”?

Immaginiamo Ethernet come un servizio postale all’interno di un edificio (la vostra LAN - Local Area Network). Quando volete inviare un documento a un collega nell’ufficio accanto, non spedite una lettera con francobollo tramite la posta nazionale - usate la posta interna dell’edificio. Mettete il documento in una busta interna, scrivete l’ufficio del destinatario, e lo consegnate al servizio di smistamento interno (lo switch Ethernet).

Il frame Ethernet è proprio questa “busta interna”: un contenitore standardizzato che permette ai dati di viaggiare all’interno della rete locale. Come una busta fisica, ha dei campi specifici dove vengono scritte informazioni cruciali per la consegna.

Storia ed evoluzione di Ethernet

Ethernet fu inventato da Robert Metcalfe e David Boggs alla Xerox PARC negli anni '70. Inizialmente era un sistema di rete semplice progettato per collegare computer nello stesso edificio. Da allora, Ethernet si è evoluto enormemente:

Nonostante queste evoluzioni enormi in termini di velocità e mezzo fisico, la struttura base del frame Ethernet è rimasta sostanzialmente la stessa, il che è una testimonianza dell’eccellente design iniziale. Questo significa che i concetti che impariamo oggi sono validi sia per una vecchia rete Ethernet a 10 Mbps sia per moderne reti da 100 Gbps.

Struttura dettagliata di un Frame Ethernet

Un frame Ethernet è composto da diversi campi, ognuno con uno scopo specifico. Analizziamoli in dettaglio:

1. Preambolo e SFD (Start Frame Delimiter)

Il preambolo è come il countdown prima del lancio di un razzo: permette al dispositivo ricevente di “agganciarsi” al segnale elettrico in arrivo e prepararsi a ricevere i dati veri e propri. È un po’ come quando un’orchestra accorda gli strumenti prima di iniziare a suonare.

2. Indirizzo MAC di Destinazione (dst)

L’indirizzo MAC (Media Access Control) è come il numero di appartamento nel nostro edificio. È un identificatore univoco assegnato a ogni scheda di rete al momento della fabbricazione. I primi 3 byte identificano il produttore (chiamato OUI - Organizationally Unique Identifier), mentre gli ultimi 3 byte sono assegnati dal produttore stesso per rendere l’indirizzo unico.

Curiosità: teoricamente, non dovrebbero esistere due schede di rete al mondo con lo stesso MAC address (anche se in pratica questo può essere aggirato via software).

Tipi di indirizzamento MAC:

3. Indirizzo MAC Sorgente (src)

È fondamentale che il mittente includa il proprio indirizzo MAC, altrimenti il destinatario non saprebbe a chi rispondere. È come mettere il mittente su una lettera.

4. EtherType / Lunghezza (type)

Valori EtherType comuni:

Valore Esadecimale Valore Decimale Protocollo
0x0800 2048 IPv4 (Internet Protocol versione 4)
0x0806 2054 ARP (Address Resolution Protocol)
0x86DD 34525 IPv6 (Internet Protocol versione 6)
0x8100 33024 VLAN tagging (802.1Q)
0x88CC 35020 LLDP (Link Layer Discovery Protocol)
0x8863 34915 PPPoE Discovery
0x8864 34916 PPPoE Session

Questo campo è cruciale perché dice al dispositivo ricevente: “Ho finito di leggere l’header Ethernet, ora i dati che seguono sono di tipo X, quindi passali al modulo appropriato per gestirli”. È come le etichette sui pacchi: “Fragile”, “Refrigerare”, ecc.

5. Payload (Dati)

Il payload è il “contenuto della busta” - ciò che effettivamente vogliamo trasportare. Potrebbe essere un pacchetto IP, una richiesta ARP, un frame di protocolli industriali come Modbus, ecc.

La limitazione a 1500 byte è chiamata MTU (Maximum Transmission Unit) di Ethernet. Se avete dati più grandi, devono essere frammentati in frame multipli. Pensatela come il limite di peso per un pacco postale: se superate il limite, dovete usare più pacchi.

6. Frame Check Sequence (FCS)

L’FCS è un codice di controllo errori. Il trasmittente calcola un valore basato su tutto il frame, e lo aggiunge alla fine. Il ricevente ricalcola questo valore: se corrisponde, il frame è integro; se non corrisponde, il frame è corrotto e viene scartato. È come un checksum, ma molto più robusto.

Importante: In Scapy, normalmente non gestiamo direttamente l’FCS perché è gestito dall’hardware della scheda di rete.

Utilizzo pratico di Ether in Scapy

Ora che abbiamo compreso la teoria, vediamo come Scapy ci permette di lavorare concretamente con i frame Ethernet.

Creare un frame Ethernet basilare:

from scapy.all import Ether

# Creare un frame Ethernet con valori di default
frame = Ether()

# Scapy imposta automaticamente alcuni valori di default
print(frame.summary())

Quando creiamo un oggetto Ether() senza specificare parametri, Scapy applica delle scelte di default intelligenti. Possiamo visualizzare tutti i campi con il metodo show():

frame.show()

Output tipico:

###[ Ethernet ]### 
  dst       = ff:ff:ff:ff:ff:ff
  src       = 00:00:00:00:00:00
  type      = 0x9000

Analizziamo questi valori:

Personalizzazione avanzata dei campi Ethernet

Possiamo specificare manualmente ogni campo del frame Ethernet per avere il controllo totale:

from scapy.all import Ether

# Creare un frame con destinazione specifica
frame_unicast = Ether(dst="aa:bb:cc:dd:ee:ff")
print(f"Questo frame è destinato a: {frame_unicast.dst}")

# Creare un frame specificando sia source che destination
frame_custom = Ether(
    dst="00:1a:2b:3c:4d:5e",  # MAC destinatario
    src="00:11:22:33:44:55"   # MAC mittente (possiamo fare spoofing!)
)

# Visualizzare la struttura completa
frame_custom.show()

Quando e perché personalizzare questi campi?

Scenario 1: Comunicazione Unicast Diretta
Se conosciamo il MAC address di un dispositivo specifico e vogliamo comunicare solo con lui (senza disturbare gli altri dispositivi della rete), impostiamo un indirizzo unicast:

# Voglio parlare solo con il dispositivo che ha questo MAC
router_mac = "00:1a:2b:3c:4d:5e"
frame = Ether(dst=router_mac)

Scenario 2: MAC Spoofing per Testing
In scenari di testing controllati, potremmo voler “fingere” di essere un altro dispositivo:

# Simulo di essere un dispositivo con un MAC specifico
# ATTENZIONE: usare solo in ambienti di test controllati!
frame = Ether(src="aa:bb:cc:dd:ee:ff", dst="00:1a:2b:3c:4d:5e")

Questo è utile per:

Scenario 3: Broadcast per Discovery
Quando vogliamo che tutti i dispositivi ricevano il nostro frame (tipico per ARP o discovery protocols):

# Messaggio broadcast
frame = Ether(dst="ff:ff:ff:ff:ff:ff")

L’arte dell’incapsulamento: stacking dei protocolli

Uno dei concetti più potenti e eleganti di Scapy è l’incapsulamento dei protocolli usando l’operatore /. Questo riflette esattamente come funzionano le reti nella realtà: ogni livello “impacchetta” il livello superiore come un suo payload.

Pensate a una bambola matrioska russa: ogni bambola contiene una bambola più piccola. Allo stesso modo, un frame Ethernet contiene un pacchetto IP, che contiene un segmento TCP, che contiene i dati dell’applicazione.

Esempio base: Ethernet + IP

from scapy.all import Ether, IP

# Creiamo un frame Ethernet che trasporta un pacchetto IP
packet = Ether() / IP(dst="192.168.1.1")

# Visualizziamo la struttura completa
packet.show()

Output:

###[ Ethernet ]### 
  dst       = ff:ff:ff:ff:ff:ff
  src       = 00:00:00:00:00:00
  type      = 0x800         # <-- Nota: automaticamente impostato a IPv4!
###[ IP ]### 
     version   = 4
     ihl       = None
     tos       = 0x0
     len       = None
     id        = 1
     flags     = 
     frag      = 0
     ttl       = 64
     proto     = hopopt
     chksum    = None
     src       = 192.168.1.10  # <-- Il nostro IP (automatico)
     dst       = 192.168.1.1    # <-- L'IP di destinazione che abbiamo specificato
     \options   \

Notate cosa è successo di magico: quando abbiamo impilato IP sopra Ether, Scapy ha automaticamente impostato il campo type di Ethernet a 0x800 (che indica IPv4). Scapy è abbastanza intelligente da comprendere le relazioni tra protocolli e configurare automaticamente i campi appropriati.

Esempio più complesso: Stack completo

from scapy.all import Ether, IP, TCP

# Costruiamo un pacchetto completo: Ethernet -> IP -> TCP
complete_packet = Ether() / IP(dst="192.168.1.100") / TCP(dport=80, sport=12345)

# Questo rappresenta una richiesta HTTP (porta 80) 
# proveniente dalla porta locale 12345

Questo singolo oggetto rappresenta uno stack protocollare completo:

Accesso ai singoli layer di un pacchetto

Quando creiamo pacchetti multi-layer, Scapy ci permette di accedere e modificare ogni singolo layer in modo indipendente:

from scapy.all import Ether, IP, TCP

# Creiamo un pacchetto complesso
packet = Ether() / IP(dst="192.168.1.100") / TCP(dport=80)

# Accedere al layer Ethernet
eth_layer = packet[Ether]
print(f"MAC destinazione: {eth_layer.dst}")

# Accedere al layer IP
ip_layer = packet[IP]
print(f"IP destinazione: {ip_layer.dst}")
print(f"TTL: {ip_layer.ttl}")

# Accedere al layer TCP
tcp_layer = packet[TCP]
print(f"Porta destinazione: {tcp_layer.dport}")
print(f"Porta sorgente: {tcp_layer.sport}")

# Modificare un campo specifico
packet[IP].ttl = 32  # Cambiamo il Time To Live
packet[TCP].flags = "S"  # Impostiamo il flag SYN

Questo meccanismo di accesso è estremamente potente perché ci permette di navigare attraverso lo stack protocollare come se fosse un oggetto Python normale.

Applicazioni pratiche reali di Ether

Vediamo alcuni scenari del mondo reale dove la manipolazione diretta dei frame Ethernet è cruciale:

1. Wake-on-LAN (WoL)

Wake-on-LAN è una tecnologia che permette di “risvegliare” computer spenti da remoto. Funziona inviando un “magic packet” - un frame Ethernet speciale contenente il MAC address del computer target ripetuto 16 volte.

from scapy.all import Ether, Raw

# MAC address del computer da risvegliare
target_mac = "00:1a:2b:3c:4d:5e"

# Costruiamo il magic packet
# Deve contenere 6 byte di 0xFF seguiti dal MAC ripetuto 16 volte
magic = b'\xff' * 6 + bytes.fromhex(target_mac.replace(':', '')) * 16

# Creiamo il frame Ethernet
wol_packet = Ether(dst="ff:ff:ff:ff:ff:ff") / Raw(load=magic)

# Inviamo il pacchetto (il computer si accenderà!)
# sendp(wol_packet, iface="eth0")

2. VLAN Tagging (802.1Q)

In reti enterprise, le VLAN (Virtual LAN) permettono di segmentare logicamente una rete fisica. Questo richiede l’aggiunta di un tag VLAN al frame Ethernet:

from scapy.all import Ether, Dot1Q, IP

# Pacchetto su VLAN 10
packet_vlan = Ether() / Dot1Q(vlan=10) / IP(dst="192.168.10.1")

# Scapy inserisce automaticamente il tag 0x8100 nell'EtherType
packet_vlan.show()

3. Analisi di traffico locale per troubleshooting

Quando una comunicazione non funziona in una LAN, spesso il problema è al livello 2. Possiamo usare Ether per costruire frame di test:

from scapy.all import Ether, ARP, srp

# Test: il dispositivo 192.168.1.50 risponde?
test_frame = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="192.168.1.50")
answered, unanswered = srp(test_frame, timeout=2, verbose=0)

if answered:
    print(f"Dispositivo trovato! MAC: {answered[0][1].hwsrc}")
else:
    print("Dispositivo non risponde - possibile problema di rete")

4. Testing di switch: MAC address table learning

Gli switch Ethernet imparano quali MAC address sono su quali porte osservando il traffico. Possiamo testare questo comportamento:

from scapy.all import Ether, sendp

# Inviamo frame con MAC sorgente diversi
for i in range(5):
    fake_mac = f"00:11:22:33:44:{i:02x}"
    test_frame = Ether(src=fake_mac, dst="ff:ff:ff:ff:ff:ff")
    # sendp(test_frame, iface="eth0")
    print(f"Inviato frame da MAC: {fake_mac}")

# Lo switch dovrebbe ora avere 5 entry diverse nella sua MAC address table

Best practices nell’uso di Ether

  1. Lasciare che Scapy gestisca il src MAC quando possibile: Il driver della NIC lo imposterà correttamente
  2. Usare broadcast solo quando necessario: Il broadcast genera traffico su tutti i dispositivi
  3. Verificare sempre l’EtherType quando si impilano protocolli: Anche se Scapy lo fa automaticamente, è bene controllare
  4. In ambienti virtualizzati, attenzione alle policy delle VM: Alcune piattaforme filtrano frame con MAC spoofati

2. ARP: Address Resolution Protocol - Il Traduttore della Rete

Il problema fondamentale che ARP risolve

Per comprendere veramente ARP (Address Resolution Protocol), dobbiamo prima capire il problema che risolve. Immaginate di essere in una grande azienda. Voi conoscete il nome di un collega (“Mario Rossi, reparto vendite”) e volete inviargli un documento cartaceo. Ma per consegnare fisicamente il documento, avete bisogno di sapere in quale ufficio si trova esattamente (“Edificio B, piano 3, ufficio 307”).

Nelle reti informatiche succede qualcosa di molto simile:

Quando il vostro computer vuole inviare dati a un altro computer, il protocollo IP sa quale indirizzo IP raggiungere (ad esempio, 192.168.1.20), ma il livello Ethernet ha bisogno dell’indirizzo MAC per costruire il frame e consegnare fisicamente i bit sulla rete locale.

Il problema: Come facciamo a scoprire l’indirizzo MAC corrispondente a un determinato indirizzo IP?

La soluzione: ARP - Address Resolution Protocol!

Come funziona ARP: una conversazione di rete

ARP è essenzialmente un protocollo di domanda-e-risposta che opera tramite broadcast. Il processo è sorprendentemente semplice ed elegante. Vediamolo passo per passo con un esempio concreto:

Scenario iniziale:

Passo 1: ARP Request (Richiesta broadcast)

Computer A crea e invia un pacchetto ARP Request:

"A TUTTI i dispositivi di questa rete:
Chi tra voi ha l'indirizzo IP 192.168.1.20?
Se sei tu, per favore rispondimi dicendomi il tuo indirizzo MAC!
Il mio IP è 192.168.1.10 e il mio MAC è aa:bb:cc:dd:ee:ff"

Questo messaggio viene inviato in broadcast (a tutti), il che significa che:

Passo 2: Ricezione ed elaborazione

Ogni dispositivo sulla rete riceve il broadcast:

Passo 3: ARP Reply (Risposta unicast)

Computer B invia una risposta diretta (unicast) a Computer A:

"Ciao Computer A (aa:bb:cc:dd:ee:ff),
sono io che ho l'IP 192.168.1.20!
Il mio indirizzo MAC è 11:22:33:44:55:66.
Ora puoi inviarmi i dati direttamente!"

Questa risposta:

Passo 4: Aggiornamento ARP Cache

Computer A riceve la risposta e:

  1. Estrae l’informazione: “192.168.1.20 = 11:22:33:44:55:66”
  2. Salva questa associazione nella sua ARP cache (tabella ARP)
  3. Ora può costruire frame Ethernet per comunicare con 192.168.1.20

Da questo momento in poi, Computer A non ha più bisogno di fare ARP requests per 192.168.1.20 (almeno finché l’entry non scade dalla cache, tipicamente dopo alcuni minuti).

La struttura dettagliata di un pacchetto ARP

Un pacchetto ARP è strutturato in modo molto specifico. Comprendere questa struttura è fondamentale per usare efficacemente Scapy. Analizziamo ogni campo in dettaglio:

1. Hardware Type (hwtype)

Questo campo esiste perché ARP non è limitato solo a Ethernet. Può funzionare anche su altre tecnologie di rete (anche se nella pratica moderna, Ethernet domina).

2. Protocol Type (ptype)

Questo campo dice “sto risolvendo indirizzi di quale protocollo”. Nella pratica è quasi sempre IPv4.

3. Hardware Address Length (hwlen)

Per Ethernet è sempre 6 (perché i MAC sono 6 byte), ma per altre tecnologie potrebbe essere diverso.

4. Protocol Address Length (plen)

Per IPv4 è sempre 4 byte. Per IPv6 sarebbe 16 byte.

5. Operation (op)

Questo è il campo più importante perché distingue tra domanda e risposta.

6. Sender Hardware Address (hwsrc)

In una request, è il MAC di chi sta chiedendo. In una reply, è il MAC di chi sta rispondendo.

7. Sender Protocol Address (psrc)

8. Target Hardware Address (hwdst)

Questo campo è interessante perché in una ARP request è tipicamente zero (è proprio l’informazione che stiamo cercando).

9. Target Protocol Address (pdst)

In una request, è l’IP di cui vogliamo scoprire il MAC. In una reply, è l’IP del richiedente.

Visualizzazione pratica: Pacchetto ARP in dettaglio

Vediamo come appare un vero pacchetto ARP in Scapy:

from scapy.all import ARP

# Creare una ARP request basilare
arp_request = ARP()
arp_request.show()

Output dettagliato:

###[ ARP ]### 
  hwtype    = 0x1           # Hardware type: Ethernet
  ptype     = 0x800         # Protocol type: IPv4
  hwlen     = 6             # Hardware address length: 6 bytes (MAC)
  plen      = 4             # Protocol address length: 4 bytes (IPv4)
  op        = who-has       # Operation: ARP request (value = 1)
  hwsrc     = 00:00:00:00:00:00  # Hardware source: vuoto (sarà riempito dalla NIC)
  psrc      = 0.0.0.0       # Protocol source: 0.0.0.0 (sarà riempito automaticamente)
  hwdst     = 00:00:00:00:00:00  # Hardware dest: vuoto (è quello che cerchiamo!)
  pdst      = 0.0.0.0       # Protocol dest: non specificato

Analizziamo questo output:

Creare una ARP Request completa e funzionale

Ora creiamo una vera ARP request che potremmo effettivamente inviare sulla rete:

from scapy.all import Ether, ARP

# Definiamo l'IP di cui vogliamo scoprire il MAC
target_ip = "192.168.1.1"  # Tipicamente il gateway/router

# STEP 1: Creiamo il layer ARP
arp_layer = ARP(pdst=target_ip)

# STEP 2: Creiamo il layer Ethernet (deve essere broadcast)
ether_layer = Ether(dst="ff:ff:ff:ff:ff:ff")

# STEP 3: Combiniamo i due layer
complete_arp_request = ether_layer / arp_layer

# Visualizziamo il pacchetto completo
print("=== PACCHETTO ARP REQUEST COMPLETO ===")
complete_arp_request.show()

Output:

=== PACCHETTO ARP REQUEST COMPLETO ===
###[ Ethernet ]### 
  dst       = ff:ff:ff:ff:ff:ff  # Broadcast
  src       = 00:00:00:00:00:00  # Sarà riempito dalla NIC
  type      = 0x806              # ARP (Scapy lo ha impostato automaticamente!)
###[ ARP ]### 
     hwtype    = 0x1
     ptype     = 0x800
     hwlen     = 6
     plen      = 4
     op        = who-has          # Request
     hwsrc     = 00:00:00:00:00:00
     psrc      = 0.0.0.0          # Sarà riempito con il nostro IP
     hwdst     = 00:00:00:00:00:00  # È quello che cerchiamo
     pdst      = 192.168.1.1      # L'IP target che vogliamo risolvere

Notate come Scapy abbia automaticamente impostato type = 0x806 nel layer Ethernet. Questo è il codice per ARP, e Scapy lo ha dedotto dal fatto che abbiamo impilato un layer ARP sopra Ethernet.

Comprendere cosa succede quando inviamo questo pacchetto

Quando invieremo questo pacchetto sulla rete (usando srp che vedremo più avanti), ecco cosa accadrà:

  1. Il nostro computer:

  2. Lo switch:

  3. Tutti i dispositivi della LAN:

  4. Il dispositivo con IP 192.168.1.1 (nel nostro esempio):

Creare una ARP Reply manualmente

Anche se normalmente non creeremo ARP replies manualmente (i dispositivi rispondono automaticamente), è istruttivo vedere come sono fatte:

from scapy.all import Ether, ARP

# Scenario: Computer B (192.168.1.20, MAC: aa:bb:cc:dd:ee:ff) 
# risponde a Computer A (192.168.1.10, MAC: 11:22:33:44:55:66)

# STEP 1: Layer Ethernet (unicast, diretto a Computer A)
ether_reply = Ether(
    dst="11:22:33:44:55:66",  # MAC di Computer A (il richiedente)
    src="aa:bb:cc:dd:ee:ff"   # Il nostro MAC (Computer B)
)

# STEP 2: Layer ARP (reply)
arp_reply = ARP(
    op=2,                       # 2 = is-at (ARP reply)
    hwsrc="aa:bb:cc:dd:ee:ff",  # Il nostro MAC
    psrc="192.168.1.20",        # Il nostro IP
    hwdst="11:22:33:44:55:66",  # MAC del richiedente
    pdst="192.168.1.10"         # IP del richiedente
)

# STEP 3: Pacchetto completo
complete_arp_reply = ether_reply / arp_reply

print("=== ARP REPLY ===")
complete_arp_reply.show()

Output:

=== ARP REPLY ===
###[ Ethernet ]### 
  dst       = 11:22:33:44:55:66  # Unicast al richiedente
  src       = aa:bb:cc:dd:ee:ff  # Noi
  type      = 0x806
###[ ARP ]### 
     hwtype    = 0x1
     ptype     = 0x800
     hwlen     = 6
     plen      = 4
     op        = is-at            # Reply (valore = 2)
     hwsrc     = aa:bb:cc:dd:ee:ff  # Il nostro MAC
     psrc      = 192.168.1.20     # Il nostro IP
     hwdst     = 11:22:33:44:55:66  # MAC del richiedente (ora lo conosciamo)
     pdst      = 192.168.1.10     # IP del richiedente

La differenza chiave con la request:

ARP Cache: il sistema di memoria dei mapping

Una caratteristica fondamentale di ARP è la cache (o tabella ARP). Sarebbe estremamente inefficiente inviare una ARP request ogni singola volta che vogliamo comunicare con un dispositivo. Invece, i sistemi operativi mantengono una tabella che memorizza i mapping IP-MAC appresi.

Visualizzare la ARP cache:

Su Linux/Mac:

arp -a

Su Windows:

arp -a

Output tipico:

? (192.168.1.1) at 00:1a:2b:3c:4d:5e [ether] on eth0
? (192.168.1.20) at aa:bb:cc:dd:ee:ff [ether] on eth0
? (192.168.1.30) at 11:22:33:44:55:66 [ether] on eth0

Questo mostra:

Lifetime delle entry ARP:

Le entry nella ARP cache non rimangono per sempre. Tipicamente:

Questo meccanismo di scadenza è importante perché:

  1. Previene l’uso di mapping obsoleti (se un dispositivo cambia IP o MAC)
  2. Libera memoria da mapping non più necessari
  3. Permette il recupero da errori temporanei

Manipolare la ARP cache da command line:

# Aggiungere una entry statica (Linux)
sudo arp -s 192.168.1.100 aa:bb:cc:dd:ee:ff

# Rimuovere una entry specifica (Linux)
sudo arp -d 192.168.1.100

# Svuotare completamente la cache (Linux)
sudo ip -s -s neigh flush all

# Windows - aggiungere entry statica
arp -s 192.168.1.100 aa-bb-cc-dd-ee-ff

# Windows - rimuovere entry
arp -d 192.168.1.100

Gratuitous ARP: annunciare la propria presenza

Un caso speciale di ARP è il Gratuitous ARP (ARP gratuito). È un ARP request in cui:

Scopi del Gratuitous ARP:

  1. Rilevare conflitti IP: Se qualcun altro risponde, significa che c’è un conflitto
  2. Aggiornare le cache altrui: Quando cambiamo MAC (es. failover), annunciamo il nuovo mapping
  3. Inizializzazione rapida: All’avvio, un dispositivo può annunciare la sua presenza

Esempio di Gratuitous ARP con Scapy:

from scapy.all import Ether, ARP

# Gratuitous ARP: annuncio "Io sono 192.168.1.50 e ho questo MAC"
gratuitous = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(
    op=1,                      # È una request
    psrc="192.168.1.50",      # Il mio IP
    pdst="192.168.1.50",      # Sto chiedendo il mio stesso IP
    hwsrc="aa:bb:cc:dd:ee:ff" # Il mio MAC
)

# Quando inviamo questo, tutti i dispositivi aggiorneranno la loro cache
# con: 192.168.1.50 -> aa:bb:cc:dd:ee:ff

ARP Probe: verificare se un IP è libero

Prima di configurare un indirizzo IP, un dispositivo intelligente dovrebbe verificare che non sia già in uso. Questo si fa con un ARP Probe:

from scapy.all import Ether, ARP

# Vogliamo usare 192.168.1.100, ma è libero?
ip_to_check = "192.168.1.100"

# ARP Probe: psrc = 0.0.0.0, pdst = IP da verificare
probe = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(
    op=1,
    psrc="0.0.0.0",          # Source IP è zero!
    pdst=ip_to_check,        # L'IP che vogliamo verificare
    hwsrc="aa:bb:cc:dd:ee:ff"  # Il nostro MAC
)

# Se qualcuno risponde, l'IP è già in uso
# Se nessuno risponde, l'IP è libero

La differenza chiave è psrc = 0.0.0.0 - questo dice “non ho ancora un IP, sto solo controllando”.

Applicazioni pratiche avanzate di ARP

1. Network Discovery avanzato con statistiche:

from scapy.all import Ether, ARP, srp
import time

def advanced_arp_discovery(network_range):
    """
    Scansione ARP con raccolta di statistiche dettagliate
    """
    print(f"Scansione ARP di {network_range}...")
    
    # Costruzione pacchetto
    arp = ARP(pdst=network_range)
    ether = Ether(dst="ff:ff:ff:ff:ff:ff")
    packet = ether / arp
    
    # Timing
    start_time = time.time()
    
    # Invio e ricezione
    answered, unanswered = srp(packet, timeout=3, verbose=0)
    
    end_time = time.time()
    elapsed = end_time - start_time
    
    # Statistiche
    total_ips = 254  # Per una /24
    found = len(answered)
    missing = len(unanswered)
    response_rate = (found / total_ips) * 100
    
    print(f"\n{'='*60}")
    print(f"RISULTATI SCANSIONE ARP")
    print(f"{'='*60}")
    print(f"Tempo impiegato: {elapsed:.2f} secondi")
    print(f"Host trovati: {found}")
    print(f"Host non raggiungibili: {missing}")
    print(f"Tasso di risposta: {response_rate:.1f}%")
    print(f"\n{'IP Address':<20} {'MAC Address':<20} {'Vendor OUI'}")
    print(f"{'-'*60}")
    
    for sent, received in answered:
        # I primi 3 byte del MAC identificano il vendor
        vendor_oui = received.hwsrc[:8]  # XX:XX:XX
        print(f"{received.psrc:<20} {received.hwsrc:<20} {vendor_oui}")
    
    return answered, unanswered

# Uso
# results = advanced_arp_discovery("192.168.1.0/24")

2. Rilevamento di ARP Spoofing/Poisoning:

ARP può essere abusato per attacchi di tipo “man-in-the-middle”. Possiamo creare uno script di monitoraggio:

from scapy.all import Ether, ARP

# Definiamo l'IP target di cui vogliamo scoprire il MAC
target_ip = "192.168.1.1"

# Creiamo il frame Ethernet (broadcast)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")

# Creiamo la ARP request
arp = ARP(pdst=target_ip)

# Combiniamo i due livelli
packet = ether/arp
packet.show()

Questo pacchetto, quando inviato, chiederà: “Chi ha l’indirizzo IP 192.168.1.1? Rispondi al mio MAC address con il tuo!”

Creare una ARP Reply

Possiamo anche creare manualmente una risposta ARP (utile per testing o per comprendere attacchi come ARP poisoning):

arp_reply = ARP(
    op=2,                               # 2 = is-at (ARP reply)
    hwsrc="aa:bb:cc:dd:ee:ff",         # Il nostro MAC
    psrc="192.168.1.100",              # Il nostro IP
    hwdst="11:22:33:44:55:66",         # MAC del richiedente
    pdst="192.168.1.50"                # IP del richiedente
)

ARP Cache e Persistenza

I sistemi operativi mantengono una ARP cache (tabella ARP) che memorizza le associazioni IP-MAC apprese. Questo evita di dover inviare una ARP request ogni volta che si comunica con lo stesso host.

Su Linux puoi visualizzare la cache ARP con:

arp -a

Su Windows:

arp -a

Applicazioni Pratiche di ARP in Scapy

  1. Network Discovery: Scoprire tutti i dispositivi attivi in una rete locale
  2. MAC Address Verification: Verificare quale dispositivo sta usando un determinato IP
  3. Testing di sicurezza: Comprendere e testare vulnerabilità come ARP spoofing/poisoning
  4. Troubleshooting: Diagnosticare problemi di risoluzione IP-MAC

Esempio Avanzato: Scansione di Rete

from scapy.all import Ether, ARP, srp

# Definiamo il range di IP da scansionare
target_range = "192.168.1.0/24"

# Creiamo il pacchetto ARP per l'intera subnet
arp = ARP(pdst=target_range)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = ether/arp

# Inviamo e riceviamo (vedremo srp più avanti)
result = srp(packet, timeout=2, verbose=0)[0]

# Elaboriamo i risultati
for sent, received in result:
    print(f"IP: {received.psrc} - MAC: {received.hwsrc}")

Questo script scansionerà tutti i 254 possibili indirizzi IP della subnet 192.168.1.0/24 e stamperà quali rispondono con il loro MAC address.


3. hexdump: Visualizzazione Esadecimale - Vedere Attraverso gli Occhi di un Computer

Perché abbiamo bisogno dell’esadecimale?

Prima di immergerci in hexdump, dobbiamo capire perché la rappresentazione esadecimale è così importante nell’informatica, e specialmente nelle reti.

I computer, a livello fondamentale, operano esclusivamente con bit - sequenze di 0 e 1. Ogni dato che viaggia su una rete, ogni file sul vostro disco, ogni istruzione eseguita dal processore, tutto è rappresentato come una serie di bit. Un pacchetto di rete, quindi, è letteralmente una sequenza di migliaia di bit che vengono trasmessi uno dopo l’altro sul mezzo fisico (cavo, fibra ottica, onde radio).

Il problema della visualizzazione:

Visualizzare i dati come sequenza pura di bit è praticamente impossibile per un essere umano:

11111111111111111111111111111111000000000000000000000000000000001000000001100000...

Questa stringa è incomprensibile. Serve un modo più pratico di rappresentare i dati binari.

Perché l’esadecimale?

Il sistema esadecimale (base 16) è perfetto per rappresentare dati binari perché:

  1. Compattezza: Un singolo digit esadecimale rappresenta esattamente 4 bit

  2. Leggibilità: Molto più facile leggere FF A3 C1 5E che 11111111101000111100000101011110

  3. Conversione diretta: Non serve fare calcoli complessi per convertire binario ↔ esadecimale

  4. Standard industriale: Tutte le specifiche di protocolli di rete usano notazione esadecimale

Tabella di conversione binario-esadecimale:

Decimale Binario Esadecimale
0 0000 0
1 0001 1
2 0010 2
3 0011 3
4 0100 4
5 0101 5
6 0110 6
7 0111 7
8 1000 8
9 1001 9
10 1010 A
11 1011 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F

Cos’è hexdump in Scapy?

hexdump è una funzione di utilità di Scapy che permette di visualizzare il contenuto grezzo (raw) di un pacchetto esattamente come viene trasmesso sulla rete, byte per byte, in formato esadecimale. È come avere i “raggi X” per i pacchetti di rete - potete vedere esattamente cosa c’è dentro.

Quando usate protocolli di alto livello (browser web, email, ecc.), il sistema operativo nasconde tutti questi dettagli. Con hexdump, invece, vedete la vera natura binaria della comunicazione di rete.

Anatomia dell’output di hexdump

Quando chiamate hexdump su un pacchetto, ottenete un output strutturato in tre sezioni principali. Analizziamolo nel dettaglio:

from scapy.all import Ether, ARP, hexdump

# Creiamo un pacchetto ARP semplice
packet = Ether(dst="ff:ff:ff:ff:ff:ff", src="aa:bb:cc:dd:ee:ff") / ARP(pdst="192.168.1.1")

# Visualizziamo in esadecimale
hexdump(packet)

Output esempio:

0000  FF FF FF FF FF FF AA BB  CC DD EE FF 08 06 00 01   ................
0010  08 00 06 04 00 01 AA BB  CC DD EE FF 00 00 00 00   ................
0020  00 00 00 00 00 00 C0 A8  01 01                     ..........

Analizziamo ogni componente di questo output:

1. COLONNA OFFSET (Prima colonna - 0000, 0010, 0020)

L’offset vi dice “a quanti byte dall’inizio del pacchetto mi trovo”. È come i numeri di riga in un documento - vi aiuta a navigare e a fare riferimento a posizioni specifiche.

Esempio:

2. COLONNA DATI ESADECIMALI (Colonne centrali)

Questa è la parte più importante - i veri dati del pacchetto. Ogni coppia di cifre rappresenta un byte.

3. COLONNA ASCII (Ultima colonna - tra i punti)

Molti byte non corrispondono a caratteri ASCII stampabili (valori < 32 o > 126), quindi vengono mostrati come . (punto).

Decodifica passo-passo di un pacchetto ARP

Prendiamo l’output sopra e decodifichiamolo campo per campo, collegando ogni byte al suo significato nel protocollo:

0000  FF FF FF FF FF FF AA BB  CC DD EE FF 08 06 00 01
      └─────┬─────┘ └─────┬──┘ └─┬─┘ └─┬─┘ └─┬─┘
         MAC dest    MAC src    EthType HwType

Byte 0-5: MAC Destination (FF FF FF FF FF FF)

Byte 6-11: MAC Source (AA BB CC DD EE FF)

Byte 12-13: EtherType (08 06)

Fin qui abbiamo il header Ethernet (14 byte totali). Ora inizia il pacchetto ARP:

0000                                              00 01
0010  08 00 06 04 00 01 AA BB  CC DD EE FF 00 00 00 00
      └─┬─┘ └┬┘└┬┘ └─┬─┘ └─────┬─────┘ └─────┬─────┘
      PType HL PL  Op    HW Src        Proto Src

Byte 14-15: Hardware Type (00 01)

Byte 16-17: Protocol Type (08 00)

Byte 18: Hardware Length (06)

Byte 19: Protocol Length (04)

Byte 20-21: Operation (00 01)

Byte 22-27: Sender Hardware Address (AA BB CC DD EE FF)

Byte 28-31: Sender Protocol Address (00 00 00 00)

0020  00 00 00 00 00 00 C0 A8  01 01
      └─────┬─────┘ └────┬────┘
         HW Dest      Proto Dest

Byte 32-37: Target Hardware Address (00 00 00 00 00 00)

Byte 38-41: Target Protocol Address (C0 A8 01 01)

Conversione manuale esadecimale ↔ decimale

È fondamentale saper convertire tra esadecimale e decimale. Vediamo i metodi:

Esadecimale → Decimale:

Metodo posizionale (per numeri multi-cifra):

C0A8 (esadecimale) = ?

C    0    A    8
12   0    10   8

= (12 × 16³) + (0 × 16²) + (10 × 16¹) + (8 × 16⁰)
= (12 × 4096) + (0 × 256) + (10 × 16) + (8 × 1)
= 49152 + 0 + 160 + 8
= 49320 (decimale)

Per byte singoli (più comune):

FF (esadecimale) = (15 × 16) + 15 = 255 (decimale)
C0 (esadecimale) = (12 × 16) + 0 = 192 (decimale)
A8 (esadecimale) = (10 × 16) + 8 = 168 (decimale)

Decimale → Esadecimale:

Metodo divisione per 16:

192 (decimale) = ? (esadecimale)

192 ÷ 16 = 12 resto 0
12 in esadecimale = C
0 in esadecimale = 0

Quindi: 192 = C0

Usando Python per verificare:

# Decimale → Esadecimale
decimal_value = 192
hex_value = hex(decimal_value)
print(hex_value)  # Output: 0xc0

# Esadecimale → Decimale
hex_value = "C0"
decimal_value = int(hex_value, 16)
print(decimal_value)  # Output: 192

# Per un indirizzo IP completo
ip_bytes = [192, 168, 1, 1]
hex_ip = ' '.join([f"{byte:02X}" for byte in ip_bytes])
print(hex_ip)  # Output: C0 A8 01 01

hexdump vs show(): quando usare quale

Scapy offre due modi principali per visualizzare pacchetti, e capire quando usare l’uno o l’altro è importante:

show() - Vista “interpretata”:

packet.show()

Vantaggi:

Usa quando:

hexdump() - Vista “grezza”:

hexdump(packet)

Vantaggi:

Usa quando:

Esempio comparativo:

from scapy.all import Ether, IP, TCP, hexdump

# Pacchetto TCP con payload
packet = Ether() / IP(dst="192.168.1.100") / TCP(dport=80, flags="S") / "Hello"

print("=== VISTA INTERPRETATA (show) ===")
packet.show()

print("\n=== VISTA GREZZA (hexdump) ===")
hexdump(packet)

La vista show() dirà “c’è un TCP SYN verso la porta 80 con payload ‘Hello’”.
La vista hexdump() mostrerà esattamente come questi dati sono codificati in byte.

Applicazioni pratiche avanzate di hexdump

1. Verificare padding e allineamento

I protocolli di rete spesso richiedono padding per allineamento. hexdump lo rivela:

from scapy.all import Ether, ARP, hexdump

# ARP con dati molto piccoli
small_arp = Ether() / ARP(pdst="192.168.1.1")
hexdump(small_arp)

# Potreste vedere byte di padding alla fine se il frame è < 64 byte
# (minimo per Ethernet)

2. Debugging di problemi di byte order (endianness)

Alcuni protocolli usano big-endian, altri little-endian:

# Valore 1234 in big-endian (network byte order)
big_endian = b'\x04\xd2'  # 04 = 4, D2 = 210 → (4 × 256) + 210 = 1234

# Valore 1234 in little-endian 
little_endian = b'\xd2\x04'  # D2 = 210, 04 = 4 → (210 + 4 × 256) = 1234

# hexdump rivela immediatamente l'ordine dei byte
from scapy.all import hexdump, Raw

hexdump(Raw(load=big_endian))
# Output: 0000  04 D2  ..

hexdump(Raw(load=little_endian))
# Output: 0000  D2 04  ..

3. Identificare stringhe nascoste in pacchetti

A volte i payload contengono stringhe di testo. La colonna ASCII di hexdump le rende visibili:

from scapy.all import IP, TCP, hexdump

# HTTP request nascosta in un pacchetto
http_packet = IP() / TCP() / "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
hexdump(http_packet)

# La colonna ASCII mostrerà "GET / HTTP/1.1" chiaramente visibile

4. Reverse engineering di protocolli sconosciuti

Se intercettate traffico di un protocollo proprietario, hexdump è il primo strumento:

from scapy.all import hexdump, sniff

# Catturate alcuni pacchetti
captured = sniff(count=1, iface="eth0")

# Analizzate la struttura byte per byte
hexdump(captured[0])

# Cercate pattern ripetuti, valori fissi (magic numbers), 
# lunghezze, checksum position, ecc.

5. Confrontare pacchetti per trovare differenze

from scapy.all import Ether, ARP, hexdump

# Due pacchetti quasi identici
packet1 = Ether() / ARP(pdst="192.168.1.1")
packet2 = Ether() / ARP(pdst="192.168.1.2")

print("=== PACCHETTO 1 ===")
hexdump(packet1)

print("\n=== PACCHETTO 2 ===")
hexdump(packet2)

# Confrontando, vedrete che cambiano solo gli ultimi byte (l'ultimo byte dell'IP)

Esportare hexdump per documentazione

A volte volete salvare l’output di hexdump per documentazione o report:

from scapy.all import Ether, ARP, hexdump
import sys

packet = Ether() / ARP(pdst="192.168.1.1")

# Salvare in un file
with open('packet_analysis.txt', 'w') as f:
    # Redirigere stdout temporaneamente
    old_stdout = sys.stdout
    sys.stdout = f
    hexdump(packet)
    sys.stdout = old_stdout

print("Hexdump salvato in packet_analysis.txt")

Strumenti complementari a hexdump

Quando fate analisi di pacchetti, hexdump si integra bene con altri strumenti:

1. bytes() - Ottenere i byte grezzi

from scapy.all import Ether, ARP

packet = Ether() / ARP(pdst="192.168.1.1")

# Ottenere i byte come oggetto Python bytes
raw_bytes = bytes(packet)
print(f"Lunghezza totale: {len(raw_bytes)} byte")
print(f"Primi 10 byte: {raw_bytes[:10]}")

# Ogni elemento è un byte (0-255)
for i, byte in enumerate(raw_bytes[:20]):
    print(f"Byte {i}: {byte:3d} (decimale) = {byte:02X} (hex) = {bin(byte)} (binario)")

2. raw() - Simile a bytes()

from scapy.all import Ether, ARP, raw

packet = Ether() / ARP(pdst="192.168.1.1")

# raw() è equivalente a bytes()
raw_data = raw(packet)

3. sprintf() - Formattazione custom

from scapy.all import Ether, ARP, IP

packet = Ether() / IP(dst="192.168.1.100") / ARP(pdst="192.168.1.1")

# Formattazione personalizzata
formatted = packet.sprintf("Ethernet: %Ether.src% -> %Ether.dst% | IP dst: %IP.dst%")
print(formatted)

Esercizi pratici con hexdump

Esercizio 1: Decodifica manuale

# Guardate questo hexdump e identificate:
# - Il tipo di pacchetto
# - Gli indirizzi MAC
# - L'indirizzo IP target

"""
0000  FF FF FF FF FF FF 11 22  33 44 55 66 08 06 00 01
0010  08 00 06 04 00 01 11 22  33 44 55 66 C0 A8 01 0A
0020  00 00 00 00 00 00 C0 A8  01 01
"""

# Risposta:
# - Pacchetto: ARP request (08 06 = EtherType ARP, 00 01 = Operation Request)
# - MAC destinazione: FF:FF:FF:FF:FF:FF (broadcast)
# - MAC sorgente: 11:22:33:44:55:66
# - IP sorgente: C0 A8 01 0A = 192.168.1.10
# - IP target: C0 A8 01 01 = 192.168.1.1

Esercizio 2: Trovare il magic number

Molti protocolli iniziano con un “magic number” - una sequenza fissa di byte che identifica il protocollo. Trovatelo:

from scapy.all import hexdump, Raw

# Un protocollo misterioso
mystery_protocol = Raw(load=b'\xCA\xFE\xBA\xBE' + b'\x00\x01\x02\x03' + b'Hello')
hexdump(mystery_protocol)

# Output: 0000  CA FE BA BE 00 01 02 03 Hello
# Magic number: CA FE BA BE (in realtà usato da Java class files!)

4. srp: Send and Receive Packets (Layer 2) - L’Arte della Comunicazione Bidirezionale

Introduzione: La necessità della comunicazione bidirezionale

Finora abbiamo imparato a costruire pacchetti (con Ether e ARP), a visualizzarli in formato strutturato (show) e grezzo (hexdump). Ma nella rete reale, la comunicazione non è mai unidirezionale - è un dialogo. Quando inviate una richiesta ARP, vi aspettate una risposta. Quando fate un ping, vi aspettate un pong. Quando inviate un SYN, vi aspettate un SYN-ACK.

srp (Send and Receive Packets) è la funzione di Scapy che implementa questa comunicazione bidirezionale al livello 2 (Data Link layer). È come avere un walkie-talkie: premete il pulsante, parlate (send), rilasciate il pulsante, ascoltate (receive).

La famiglia delle funzioni Send/Receive in Scapy

Scapy offre diverse varianti per inviare e ricevere pacchetti. Comprendere le differenze è fondamentale:

1. send() / recv() - Livello 3 (Network)

from scapy.all import IP, ICMP, send

# Invia un ping (ICMP) - routing automatico
packet = IP(dst="8.8.8.8") / ICMP()
send(packet)  # Solo invio, nessuna ricezione

2. sendp() / sniff() - Livello 2 (Data Link)

from scapy.all import Ether, ARP, sendp

# Invia un frame Ethernet senza aspettare risposta
frame = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="192.168.1.1")
sendp(frame, iface="eth0")  # Solo invio

3. sr() / sr1() - Livello 3 con ricezione

from scapy.all import IP, ICMP, sr1

# Invia ping e aspetta risposta
packet = IP(dst="192.168.1.1") / ICMP()
response = sr1(packet, timeout=2)  # Ritorna solo la prima risposta
if response:
    response.show()

4. srp() / srp1() - Livello 2 con ricezione ← IL NOSTRO FOCUS

from scapy.all import Ether, ARP, srp

# Invia ARP request e aspetta risposte
packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="192.168.1.0/24")
answered, unanswered = srp(packet, timeout=2)  # Ritorna entrambe le liste

Tabella riassuntiva:

Funzione Livello Invia Riceve Ritorna Uso Principale
send() 3 (IP) - Fire-and-forget IP
sendp() 2 (Ethernet) - Fire-and-forget Ethernet
sr() 3 (IP) (answered, unanswered) Dialogo IP
sr1() 3 (IP) prima risposta Dialogo IP singolo
srp() 2 (Ethernet) (answered, unanswered) Dialogo Ethernet
srp1() 2 (Ethernet) prima risposta Dialogo Ethernet singolo

Anatomia di srp: Come funziona internamente

Quando chiamate srp(), Scapy esegue una sequenza complessa di operazioni. Comprendere questi passaggi aiuta a usare la funzione in modo efficace:

FASE 1: PREPARAZIONE

answered, unanswered = srp(packet, timeout=2, verbose=1)

Scapy prende il vostro pacchetto (o lista di pacchetti) e prepara:

  1. Un buffer per i pacchetti inviati
  2. Un buffer per le risposte ricevute
  3. Un timer per il timeout
  4. Un filtro BPF (Berkeley Packet Filter) per catturare solo le risposte rilevanti

FASE 2: INVIO

Per ogni pacchetto da inviare:
    1. Serializza il pacchetto in byte
    2. Riempie i campi automatici (src MAC, checksums, ecc.)
    3. Invia sul wire tramite socket raw
    4. Memorizza timestamp di invio
    5. Aggiunge a lista "waiting for answer"

Se avete specificato inter=0.1, Scapy aspetterà 100ms tra un invio e l’altro.

FASE 3: RICEZIONE (ASCOLTO)

Mentre timer < timeout:
    1. Ascolta l'interfaccia di rete
    2. Cattura ogni pacchetto che arriva
    3. Verifica se il pacchetto è una risposta a una delle nostre richieste
    4. Se sì:
        - Rimuove dalla lista "waiting for answer"
        - Aggiunge a "answered" come tupla (sent, received)
    5. Se no:
        - Ignora il pacchetto

Il meccanismo per “verificare se è una risposta” si basa sul fatto che Scapy controlla se i pacchetti sono correlati (stessa conversazione).

FASE 4: TIMEOUT E CLEANUP

Quando scade il timeout:
    1. Smette di ascoltare
    2. Tutti i pacchetti in "waiting for answer" diventano "unanswered"
    3. Ritorna entrambe le liste

Struttura del valore di ritorno di srp

srp restituisce una tupla di due elementi. Analizziamo in dettaglio cosa contengono:

answered, unanswered = srp(packet, timeout=2)

1. answered - Lista di coppie (sent, received)

Tipo: SRPResult (si comporta come una lista di tuple)

Struttura:

[(packet_sent_1, packet_received_1),
 (packet_sent_2, packet_received_2),
 (packet_sent_3, packet_received_3),
 ...]

Ogni elemento è una tupla dove:

Iterazione su answered:

# Metodo 1: Unpacking diretto
for sent, received in answered:
    print(f"Ho inviato: {sent.summary()}")
    print(f"Ho ricevuto: {received.summary()}")
    print(f"IP sorgente risposta: {received.psrc}")
    print(f"MAC sorgente risposta: {received.hwsrc}")
    print()

# Metodo 2: Accesso tramite indice
for i in range(len(answered)):
    sent_packet = answered[i][0]      # Il pacchetto inviato
    received_packet = answered[i][1]  # La risposta ricevuta
    print(f"Coppia {i}: {sent_packet.summary()} -> {received_packet.summary()}")

# Metodo 3: Accedere a una specifica coppia
first_sent, first_received = answered[0]  # Prima coppia
last_sent, last_received = answered[-1]    # Ultima coppia

2. unanswered - Lista di pacchetti senza risposta

Tipo: PacketList

Struttura:

[packet_1, packet_2, packet_3, ...]

Sono i pacchetti che abbiamo inviato ma per i quali NON abbiamo ricevuto risposta entro il timeout.

Iterazione su unanswered:

for packet in unanswered:
    print(f"Nessuna risposta per: {packet.summary()}")
    # Possiamo accedere ai campi del pacchetto
    if ARP in packet:
        print(f"  Target IP era: {packet[ARP].pdst}")

Parametri di srp: Controllo fine del comportamento

srp accetta numerosi parametri che permettono un controllo granulare. Analizziamoli tutti:

srp(
    x,                    # Pacchetto(i) da inviare
    timeout=2,            # Timeout in secondi
    iface=None,           # Interfaccia di rete
    inter=0,              # Intervallo tra pacchetti
    verbose=None,         # Livello di verbosità
    chainCC=False,        # Gestione Ctrl+C
    retry=0,              # Numero di retry
    multi=False,          # Accettare risposte multiple
    filter=None,          # Filtro BPF custom
    promisc=None,         # Modalità promiscua
    store=True,           # Memorizzare pacchetti
    **kargs              # Altri parametri
)

1. x - Pacchetto(i) da inviare

Può essere:

# Singolo pacchetto
single = Ether() / ARP(pdst="192.168.1.1")
srp(single, timeout=2)

# Lista di pacchetti
packet_list = [
    Ether() / ARP(pdst="192.168.1.1"),
    Ether() / ARP(pdst="192.168.1.2"),
    Ether() / ARP(pdst="192.168.1.3")
]
srp(packet_list, timeout=2)

2. timeout - Tempo massimo di attesa

# Timeout breve per rete locale veloce
srp(packet, timeout=0.5)

# Timeout lungo per rete lenta o remota
srp(packet, timeout=5)

# Nessun timeout (aspetta indefinitamente - PERICOLOSO!)
srp(packet, timeout=None)

Scegliere il timeout giusto:

3. iface - Interfaccia di rete

# Visualizzare interfacce disponibili
from scapy.all import get_if_list, conf
print(get_if_list())  # Lista tutte le interfacce
print(conf.iface)     # Interfaccia di default

# Usare interfaccia specifica
srp(packet, iface="eth0", timeout=2)
srp(packet, iface="wlan0", timeout=2)

Perché specificare l’interfaccia:

4. inter - Intervallo tra pacchetti

# Invia immediatamente (default)
srp(packet_list, inter=0, timeout=2)

# 100ms tra ogni pacchetto
srp(packet_list, inter=0.1, timeout=2)  # 10 pacchetti/secondo

# 1 secondo tra ogni pacchetto
srp(packet_list, inter=1, timeout=5)    # 1 pacchetto/secondo

Quando usare inter:

5. verbose - Livello di output

# Silenzioso - nessun output
answered, unanswered = srp(packet, verbose=0, timeout=2)

# Normale - mostra riepilogo
answered, unanswered = srp(packet, verbose=1, timeout=2)
# Output: "Begin emission: ... Finished sending ... Received X packets"

# Dettagliato - mostra ogni pacchetto
answered, unanswered = srp(packet, verbose=2, timeout=2)
# Mostra ogni invio e ricezione in tempo reale

6. retry - Numero di tentativi

# Nessun retry (default)
srp(packet, retry=0, timeout=2)

# 2 retry (3 invii totali per ogni pacchetto)
srp(packet, retry=2, timeout=2)

Come funziona retry:

Invio 1: [packet1] [packet2] [packet3]
Aspetta timeout...
packet2 non ha risposto

Invio 2 (retry 1): [packet2]
Aspetta timeout...
packet2 ancora non risponde

Invio 3 (retry 2): [packet2]
Aspetta timeout...
packet2 → unanswered

7. multi - Risposte multiple

# Default: Solo la prima risposta per pacchetto
answered, unanswered = srp(packet, multi=False, timeout=2)
len(answered)  # Massimo N (quanti pacchetti inviati)

# Multi: Accetta tutte le risposte
answered, unanswered = srp(packet, multi=True, timeout=2)
len(answered)  # Può essere > N (se più dispositivi rispondono)

Quando usare multi=True:

Esempio completo: ARP Scan dettagliato

Mettiamo insieme tutto quello che abbiamo imparato in uno script completo e commentato:

from scapy.all import Ether, ARP, srp
import time

def comprehensive_arp_scan(target_range, timeout=3, inter=0.05):
    """
    Scansione ARP completa con analisi dettagliata
    
    Args:
        target_range (str): Range IP in notazione CIDR (es. "192.168.1.0/24")
        timeout (int): Secondi di attesa per le risposte
        inter (float): Secondi tra l'invio di pacchetti successivi
    
    Returns:
        tuple: (answered, unanswered, statistics)
    """
    
    print(f"\n{'='*70}")
    print(f"SCANSIONE ARP AVANZATA")
    print(f"{'='*70}")
    print(f"Target Range: {target_range}")
    print(f"Timeout: {timeout}s")
    print(f"Intervallo tra pacchetti: {inter}s")
    print(f"{'='*70}\n")
    
    # ===== FASE 1: COSTRUZIONE PACCHETTO =====
    print("[FASE 1] Costruzione pacchetto ARP...")
    
    # Layer Ethernet: broadcast per raggiungere tutti
    ether_layer = Ether(dst="ff:ff:ff:ff:ff:ff")
    print(f"  • Layer Ethernet: dst={ether_layer.dst} (broadcast)")
    
    # Layer ARP: richiesta per il range specificato
    arp_layer = ARP(pdst=target_range)
    print(f"  • Layer ARP: pdst={arp_layer.pdst}")
    print(f"  • Operation: {arp_layer.op} (who-has)")
    
    # Combinazione dei layer
    packet = ether_layer / arp_layer
    print(f"  • Pacchetto totale: {len(bytes(packet))} byte")
    
    # Calcolo numero di host da scansionare
    # Per una /24: 256 IP - 2 (network e broadcast) = 254 host
    if target_range.endswith('/24'):
        expected_hosts = 254
    else:
        # Calcolo approssimativo per altre subnet
        import ipaddress
        network = ipaddress.ip_network(target_range, strict=False)
        expected_hosts = network.num_addresses - 2
    
    print(f"  • Host da scansionare: {expected_hosts}")
    estimated_time = expected_hosts * inter + timeout
    print(f"  • Tempo stimato: {estimated_time:.1f} secondi")
    
    # ===== FASE 2: INVIO E RICEZIONE =====
    print(f"\n[FASE 2] Invio pacchetti ARP e ascolto risposte...")
    print(f"  • Inizio scansione alle {time.strftime('%H:%M:%S')}")
    
    # Timestamp iniziale
    start_time = time.time()
    
    # INVIO effettivo con srp
    try:
        answered, unanswered = srp(
            packet,
            timeout=timeout,
            inter=inter,
            verbose=1,        # Mostra progresso
            iface=None,       # Interfaccia default
            retry=0,          # Nessun retry
            multi=False       # Una risposta per host
        )
    except PermissionError:
        print("\n[ERRORE] Permessi insufficienti!")
        print("Esegui lo script con privilegi elevati:")
        print("  • Linux/Mac: sudo python3 script.py")
        print("  • Windows: Esegui come amministratore")
        return None, None, None
    except KeyboardInterrupt:
        print("\n\n[INTERROTTO] Scansione interrotta dall'utente")
        return None, None, None
    except Exception as e:
        print(f"\n[ERRORE] Errore durante la scansione: {e}")
        return None, None, None
    
    # Timestamp finale
    end_time = time.time()
    elapsed_time = end_time - start_time
    
    print(f"  • Fine scansione alle {time.strftime('%H:%M:%S')}")
    print(f"  • Tempo effettivo: {elapsed_time:.2f} secondi")
    
    # ===== FASE 3: ANALISI STATISTICHE =====
    print(f"\n[FASE 3] Analisi risultati...")
    
    num_answered = len(answered)
    num_unanswered = len(unanswered)
    total_sent = num_answered + num_unanswered
    
    response_rate = (num_answered / total_sent * 100) if total_sent > 0 else 0
    packets_per_second = total_sent / elapsed_time if elapsed_time > 0 else 0
    
    # Statistiche
    stats = {
        'total_sent': total_sent,
        'answered': num_answered,
        'unanswered': num_unanswered,
        'response_rate': response_rate,
        'elapsed_time': elapsed_time,
        'packets_per_second': packets_per_second
    }
    
    # ===== FASE 4: PRESENTAZIONE RISULTATI =====
    print(f"\n{'='*70}")
    print("RISULTATI SCANSIONE")
    print(f"{'='*70}")
    print(f"Pacchetti inviati:        {stats['total_sent']}")
    print(f"Risposte ricevute:        {stats['answered']}")
    print(f"Mancate risposte:         {stats['unanswered']}")
    print(f"Tasso di risposta:        {stats['response_rate']:.1f}%")
    print(f"Tempo impiegato:          {stats['elapsed_time']:.2f}s")
    print(f"Pacchetti/secondo:        {stats['packets_per_second']:.1f}")
    print(f"{'='*70}\n")
    
    if num_answered > 0:
        print(f"{'IP ADDRESS':<18} {'MAC ADDRESS':<20} {'VENDOR OUI':<15} {'LATENCY'}")
        print(f"{'-'*70}")
        
        for sent, received in answered:
            # Estrazione dati
            ip_addr = received.psrc
            mac_addr = received.hwsrc
            vendor_oui = mac_addr[:8]  # Primi 3 byte (XX:XX:XX)
            
            # Calcolo latenza (differenza tra invio e ricezione)
            latency = (received.time - sent.sent_time) * 1000  # in millisecondi
            
            print(f"{ip_addr:<18} {mac_addr:<20} {vendor_oui:<15} {latency:.2f}ms")
        
        # ===== ANALISI AGGIUNTIVE =====
        print(f"\n{'='*70}")
        print("ANALISI AGGIUNTIVE")
        print(f"{'='*70}")
        
        # Host più veloce a rispondere
        fastest = min(answered, key=lambda x: x[1].time - x[0].sent_time)
        fastest_ip = fastest[1].psrc
        fastest_latency = (fastest[1].time - fastest[0].sent_time) * 1000
        print(f"Host più veloce:          {fastest_ip} ({fastest_latency:.2f}ms)")
        
        # Host più lento a rispondere
        slowest = max(answered, key=lambda x: x[1].time - x[0].sent_time)
        slowest_ip = slowest[1].psrc
        slowest_latency = (slowest[1].time - slowest[0].sent_time) * 1000
        print(f"Host più lento:           {slowest_ip} ({slowest_latency:.2f}ms)")
        
        # Latenza media
        avg_latency = sum((r[1].time - r[0].sent_time) for r in answered) / len(answered) * 1000
        print(f"Latenza media:            {avg_latency:.2f}ms")
        
        # Analisi vendor (primi 3 byte del MAC identificano il produttore)
        vendors = {}
        for _, received in answered:
            oui = received.hwsrc[:8]
            vendors[oui] = vendors.get(oui, 0) + 1
        
        print(f"\nDistribuzione per vendor:")
        for vendor, count in sorted(vendors.items(), key=lambda x: x[1], reverse=True):
            percentage = (count / num_answered) * 100
            print(f"  • {vendor}: {count} dispositivi ({percentage:.1f}%)")
        
    else:
        print("⚠ NESSUN HOST HA RISPOSTO")
        print("\nPossibili cause:")
        print("  • Range IP non corretto")
        print("  • Host non presenti in rete")
        print("  • Firewall che blocca ARP")
        print("  • Interfaccia di rete sbagliata")
        print("\nSuggerimenti:")
        print("  • Verifica la connessione di rete")
        print("  • Controlla che il range IP sia corretto")
        print("  • Prova ad aumentare il timeout")
        print(f"  • Specifica l'interfaccia con iface='eth0'")
    
    print(f"\n{'='*70}\n")
    
    return answered, unanswered, stats


# ===== ESEMPIO DI UTILIZZO =====
if __name__ == "__main__":
    # Configura il tuo range IP
    target_network = "192.168.1.0/24"  # Modifica secondo la tua rete
    
    # Esegui la scansione
    results = comprehensive_arp_scan(
        target_network,
        timeout=3,      # 3 secondi di attesa
        inter=0.05      # 50ms tra pacchetti (20 pacchetti/secondo)
    )
    
    if results[0] is not None:
        answered, unanswered, statistics = results
        
        # Puoi fare ulteriori elaborazioni sui risultati
        print("Esempi di ulteriori analisi:\n")
        
        # 1. Salvare gli IP attivi in un file
        with open('active_hosts.txt', 'w') as f:
            for sent, received in answered:
                f.write(f"{received.psrc}\n")
        print("• IP attivi salvati in 'active_hosts.txt'")
        
        # 2. Controllare se un IP specifico è attivo
        target_check = "192.168.1.1"
        is_active = any(received.psrc == target_check for _, received in answered)
        print(f"• {target_check} è {'ATTIVO' if is_active else 'NON ATTIVO'}")
        
        # 3. Contare dispositivi per subnet
        subnets = {}
        for _, received in answered:
            subnet = '.'.join(received.psrc.split('.')[:3])
            subnets[subnet] = subnets.get(subnet, 0) + 1
        print(f"• Dispositivi per subnet: {subnets}")

Output esempio dello script

Quando eseguite lo script sopra, otterrete un output simile a questo:

======================================================================
SCANSIONE ARP AVANZATA
======================================================================
Target Range: 192.168.1.0/24
Timeout: 3s
Intervallo tra pacchetti: 0.05s
======================================================================

[FASE 1] Costruzione pacchetto ARP...
  • Layer Ethernet: dst=ff:ff:ff:ff:ff:ff (broadcast)
  • Layer ARP: pdst=192.168.1.0/24
  • Operation: who-has (who-has)
  • Pacchetto totale: 42 byte
  • Host da scansionare: 254
  • Tempo stimato: 15.7 secondi

[FASE 2] Invio pacchetti ARP e ascolto risposte...
  • Inizio scansione alle 14:23:45
Begin emission:
Finished sending 254 packets.
.*.*.*.*.*.*.*.*
Received 8 packets, got 8 answers, remaining 246 packets
  • Fine scansione alle 14:23:58
  • Tempo effettivo: 13.42 secondi

[FASE 3] Analisi risultati...

======================================================================
RISULTATI SCANSIONE
======================================================================
Pacchetti inviati:        254
Risposte ricevute:        8
Mancate risposte:         246
Tasso di risposta:        3.1%
Tempo impiegato:          13.42s
Pacchetti/secondo:        18.9
======================================================================

IP ADDRESS         MAC ADDRESS          VENDOR OUI      LATENCY
----------------------------------------------------------------------
192.168.1.1        00:1a:2b:3c:4d:5e    00:1a:2b        2.34ms
192.168.1.10       aa:bb:cc:dd:ee:ff    aa:bb:cc        5.67ms
192.168.1.15       11:22:33:44:55:66    11:22:33        3.21ms
192.168.1.20       ff:ee:dd:cc:bb:aa    ff:ee:dd        4.89ms
192.168.1.25       12:34:56:78:9a:bc    12:34:56        6.12ms
192.168.1.50       98:76:54:32:10:fe    98:76:54        2.98ms
192.168.1.100      aa:11:bb:22:cc:33    aa:11:bb        7.45ms
192.168.1.200      de:ad:be:ef:ca:fe    de:ad:be        3.76ms

======================================================================
ANALISI AGGIUNTIVE
======================================================================
Host più veloce:          192.168.1.1 (2.34ms)
Host più lento:           192.168.1.100 (7.45ms)
Latenza media:            4.55ms

Distribuzione per vendor:
  • aa:bb:cc: 2 dispositivi (25.0%)
  • 00:1a:2b: 1 dispositivi (12.5%)
  • 11:22:33: 1 dispositivi (12.5%)
  • ff:ee:dd: 1 dispositivi (12.5%)
  • 12:34:56: 1 dispositivi (12.5%)
  • 98:76:54: 1 dispositivi (12.5%)
  • de:ad:be: 1 dispositivi (12.5%)

======================================================================

Esempi di ulteriori analisi:

• IP attivi salvati in 'active_hosts.txt'
• 192.168.1.1 è ATTIVO
• Dispositivi per subnet: {'192.168.1': 8}

Interpretazione dei risultati

Tasso di risposta 3.1%: In una /24 ci sono 254 host possibili. Solo 8 hanno risposto, quindi solo l’3.1% degli indirizzi IP è attivamente in uso. Questo è normale in molte reti domestiche o piccole aziende.

Latenza: I tempi di risposta (2-7ms) indicano una rete locale in buona salute. Latenze superiori a 20-30ms potrebbero indicare problemi di congestione o dispositivi lenti.

Vendor OUI: I primi 3 byte del MAC identificano il produttore. Dispositivi con lo stesso OUI probabilmente vengono dallo stesso vendor.

Gestione degli errori comuni

Quando si usa srp in produzione o in script automatizzati, è fondamentale gestire gli errori in modo robusto:

Errore 1: Permission Denied (Errno 13)

try:
    answered, unanswered = srp(packet, timeout=2, verbose=0)
except PermissionError:
    print("[ERRORE] Privilegi insufficienti")
    print("Soluzione:")
    print("  Linux/Mac: sudo python3 script.py")
    print("  Windows: Esegui come Amministratore")
    sys.exit(1)

Errore 2: Interface Not Found

from scapy.all import get_if_list

try:
    answered, unanswered = srp(packet, iface="eth999", timeout=2)
except OSError as e:
    print(f"[ERRORE] Interfaccia non trovata: {e}")
    print("Interfacce disponibili:")
    for iface in get_if_list():
        print(f"  • {iface}")
    sys.exit(1)

Errore 3: Keyboard Interrupt (Ctrl+C)

try:
    answered, unanswered = srp(packet, timeout=10, verbose=1)
except KeyboardInterrupt:
    print("\n[INTERROTTO] Scansione fermata dall'utente")
    print("Risultati parziali:")
    # Puoi comunque processare quello che hai raccolto finora

Errore 4: Timeout troppo breve (nessuna risposta)

answered, unanswered = srp(packet, timeout=2, verbose=0)

if len(answered) == 0:
    print("[WARNING] Nessuna risposta ricevuta")
    print("Possibili cause:")
    print("  • Timeout troppo breve")
    print("  • Range IP sbagliato")
    print("  • Interfaccia di rete sbagliata")
    print("  • Firewall che blocca")
    print("\nSuggerimenti:")
    print("  • Aumenta timeout a 5 secondi")
    print("  • Verifica con: ping 192.168.1.1")
    print("  • Controlla interfaccia con: ip addr show")

Best Practices per l’uso di srp

1. Sempre gestire eccezioni

import sys

def safe_srp(packet, **kwargs):
    """
    Wrapper sicuro per srp con gestione errori
    """
    try:
        return srp(packet, **kwargs)
    except PermissionError:
        print("Errore: privilegi insufficienti")
        sys.exit(1)
    except KeyboardInterrupt:
        print("\nInterrotto dall'utente")
        sys.exit(0)
    except Exception as e:
        print(f"Errore imprevisto: {e}")
        sys.exit(1)

# Uso
answered, unanswered = safe_srp(packet, timeout=2, verbose=0)

2. Usare verbose=0 in script automatizzati

# Script interattivo - mostra progresso
if __name__ == "__main__":
    answered, unanswered = srp(packet, timeout=2, verbose=1)

# Script automatizzato/cron job - silenzioso
else:
    answered, unanswered = srp(packet, timeout=2, verbose=0)

3. Implementare rate limiting per reti grandi

# Per una /16 (65534 host), non saturare la rete
answered, unanswered = srp(
    packet,
    timeout=5,
    inter=0.1,  # 100ms tra pacchetti = 10 pacchetti/secondo
    verbose=0
)

# Tempo stimato: 65534 * 0.1 = 6553 secondi ≈ 109 minuti
# Considerare di dividere in subnet più piccole

4. Logging dei risultati

import logging
import json
from datetime import datetime

logging.basicConfig(level=logging.INFO)

def scan_and_log(target_range):
    packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=target_range)
    answered, unanswered = srp(packet, timeout=2, verbose=0)
    
    # Prepara dati per logging
    scan_result = {
        'timestamp': datetime.now().isoformat(),
        'target': target_range,
        'found': len(answered),
        'missing': len(unanswered),
        'hosts': [
            {'ip': r.psrc, 'mac': r.hwsrc}
            for s, r in answered
        ]
    }
    
    # Log in formato JSON
    logging.info(json.dumps(scan_result, indent=2))
    
    return answered, unanswered

5. Cache dei risultati per evitare scansioni ripetute

import pickle
import os
from datetime import datetime, timedelta

CACHE_FILE = 'arp_cache.pkl'
CACHE_DURATION = timedelta(minutes=5)

def scan_with_cache(target_range):
    # Controlla se esiste cache valida
    if os.path.exists(CACHE_FILE):
        with open(CACHE_FILE, 'rb') as f:
            cached = pickle.load(f)
            if datetime.now() - cached['timestamp'] < CACHE_DURATION:
                print(f"Uso cache (età: {datetime.now() - cached['timestamp']})")
                return cached['answered'], cached['unanswered']
    
    # Scansione nuova
    print("Eseguo nuova scansione...")
    packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=target_range)
    answered, unanswered = srp(packet, timeout=2, verbose=0)
    
    # Salva in cache
    with open(CACHE_FILE, 'wb') as f:
        pickle.dump({
            'timestamp': datetime.now(),
            'answered': answered,
            'unanswered': unanswered
        }, f)
    
    return answered, unanswered

Confronto srp vs altri metodi di discovery

srp (ARP Scan):

sr (IP Scan con ICMP):

from scapy.all import IP, ICMP, sr

# IP Scan alternativo
packet = IP(dst="8.8.8.8")/ICMP()
answered, unanswered = sr(packet, timeout=2)

nmap (tool esterno):

Quando usare cosa:

Caso d’uso avanzato: Monitoraggio continuo di rete

from scapy.all import Ether, ARP, srp
import time
from datetime import datetime

def continuous_network_monitor(target_range, interval=60):
    """
    Monitora continuamente la rete e rileva nuovi dispositivi
    """
    known_hosts = set()
    scan_count = 0
    
    print(f"Avvio monitoraggio continuo di {target_range}")
    print(f"Scansione ogni {interval} secondi")
    print("Premi Ctrl+C per fermare\n")
    
    try:
        while True:
            scan_count += 1
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            
            print(f"[{timestamp}] Scansione #{scan_count}...")
            
            # Esegui scansione
            packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=target_range)
            answered, unanswered = srp(packet, timeout=2, verbose=0, inter=0.05)
            
            # Estrai IP attivi
            current_hosts = set(received.psrc for _, received in answered)
            
            # Rileva nuovi host
            new_hosts = current_hosts - known_hosts
            if new_hosts:
                print(f"  ⚠️  NUOVI DISPOSITIVI RILEVATI: {new_hosts}")
                for ip in new_hosts:
                    # Trova il MAC del nuovo host
                    mac = next(r.hwsrc for s, r in answered if r.psrc == ip)
                    print(f"      • {ip} - {mac}")
            
            # Rileva host scomparsi
            disappeared = known_hosts - current_hosts
            if disappeared:
                print(f"  ⚠️  DISPOSITIVI SCOMPARSI: {disappeared}")
            
            # Aggiorna known hosts
            known_hosts = current_hosts
            
            print(f"  ✓ Host attivi: {len(current_hosts)}")
            
            # Attendi prima della prossima scansione
            time.sleep(interval)
            
    except KeyboardInterrupt:
        print(f"\n\nMonitoraggio terminato dopo {scan_count} scansioni")
        print(f"Ultimo stato: {len(known_hosts)} host attivi")

# Avvio
# continuous_network_monitor("192.168.1.0/24", interval=60)

Troubleshooting avanzato di srp

Problema: srp non cattura risposte su VM/Docker

Soluzione: Configurare la scheda di rete in modalità promiscua

# Verificare modalità promiscua
from scapy.all import conf
print(f"Promiscuous mode: {conf.promisc}")

# Forzare modalità promiscua
conf.promisc = True

# Oppure specificare nel parametro srp
answered, unanswered = srp(packet, promisc=True, timeout=2)

Problema: Su Windows, srp non funziona

Soluzione: Installare WinPcap o Npcap

# Download Npcap da: https://npcap.com/
# Installare con opzione "WinPcap Compatibility Mode"

Verifica installazione:

from scapy.all import show_interfaces
show_interfaces()  # Dovrebbe mostrare le interfacce

Problema: srp riceve risposte duplicate

Causa: Switch che fanno flooding o broadcast storms

Soluzione: Usare multi=False (default) e filtrare duplicati

answered, unanswered = srp(packet, timeout=2, multi=False)

# Filtrare manualmente duplicati per IP
seen_ips = set()
unique_answers = []
for sent, received in answered:
    if received.psrc not in seen_ips:
        seen_ips.add(received.psrc)
        unique_answers.append((sent, received))

Problema: Latenza molto alta (>100ms) su LAN

Possibili cause:

  1. Rete congestionata
  2. Switch vecchi/sovraccarichi
  3. Troppi broadcast simultanei
  4. Cavi difettosi

Diagnosi:

# Misurare latenza individuale
for sent, received in answered:
    latency = (received.time - sent.sent_time) * 1000
    if latency > 100:
        print(f"⚠️  {received.psrc}: latenza alta {latency:.2f}ms")

from scapy.all import Ether, ARP, srp
import time

def advanced_network_scan(network_range):
    """
    Scansione di rete con statistiche dettagliate
    """
    # Preparazione
    arp = ARP(pdst=network_range)
    ether = Ether(dst="ff:ff:ff:ff:ff:ff")
    packet = ether/arp
    
    # Statistiche
    start_time = time.time()
    
    # Esecuzione scan
    answered, unanswered = srp(
        packet,
        timeout=3,
        verbose=0,
        inter=0.05  # 50ms tra pacchetti per non saturare la rete
    )
    
    end_time = time.time()
    
    # Risultati
    print("=" * 60)
    print("NETWORK SCAN REPORT")
    print("=" * 60)
    print(f"Range scansionato: {network_range}")
    print(f"Tempo di esecuzione: {end_time - start_time:.2f} secondi")
    print(f"Host attivi: {len(answered)}")
    print(f"Host non raggiungibili: {len(unanswered)}")
    print("\n" + "=" * 60)
    print(f"{'IP ADDRESS':<20} {'MAC ADDRESS':<20} {'VENDOR'}")
    print("=" * 60)
    
    for sent, received in answered:
        # Puoi integrare lookup del vendor dal MAC
        print(f"{received.psrc:<20} {received.hwsrc:<20}")
    
    print("=" * 60)
    
    return answered, unanswered

# Utilizzo
results = advanced_network_scan("192.168.1.0/24")

Differenze tra srp e send

È importante distinguere tra srp() (send and receive) e sendp() (solo send):

from scapy.all import sendp, srp

# sendp() - Solo invio, nessuna ricezione
sendp(packet, iface="eth0")
# Utile per: generazione di traffico, DoS testing (etico), simulazioni

# srp() - Invio E ricezione
answered, unanswered = srp(packet, iface="eth0", timeout=2)
# Utile per: discovery, probing, testing con verifica di risposta

Best Practices con srp

  1. Timeout adeguato: Imposta un timeout ragionevole in base alla dimensione della rete

    # Rete piccola (< 50 host)
    srp(packet, timeout=1)
    
    # Rete media (50-200 host)
    srp(packet, timeout=2)
    
    # Rete grande (> 200 host)
    srp(packet, timeout=3)
    
  2. Rate limiting: Non saturare la rete con troppi pacchetti simultanei

    srp(packet, inter=0.1)  # 10 pacchetti/secondo
    
  3. Gestione errori: Sempre gestire eccezioni in ambiente di produzione

    try:
        answered, unanswered = srp(packet, timeout=2, verbose=0)
    except PermissionError:
        print("Errore: sono necessari privilegi di amministratore")
    except Exception as e:
        print(f"Errore durante l'invio: {e}")
    
  4. Interfaccia specifica: In sistemi con più interfacce, specifica quale usare

    srp(packet, iface="eth0", timeout=2)
    

5. Esempio Completo Integrato

Mettiamo insieme tutto quello che abbiamo imparato in uno script completo e commentato:

#!/usr/bin/env python3
"""
Script di Network Discovery e Analysis
Utilizza Scapy per scoprire e analizzare host in una rete locale
"""

from scapy.all import Ether, ARP, srp, hexdump
import sys

def discover_network(target_range):
    """
    Esegue una scansione ARP della rete e visualizza risultati dettagliati
    
    Args:
        target_range (str): Range IP in notazione CIDR (es. "192.168.1.0/24")
    
    Returns:
        tuple: (answered, unanswered) - risultati della scansione
    """
    
    print(f"\n{'=' * 70}")
    print(f"SCANSIONE RETE: {target_range}")
    print(f"{'=' * 70}\n")
    
    # FASE 1: Costruzione del pacchetto
    print("[FASE 1] Costruzione pacchetto ARP...")
    
    # Layer 2: Frame Ethernet (broadcast)
    ether_layer = Ether(dst="ff:ff:ff:ff:ff:ff")
    
    # Layer ARP: Request per il range specificato
    arp_layer = ARP(pdst=target_range)
    
    # Combinazione dei layer
    packet = ether_layer / arp_layer
    
    # Visualizzazione struttura pacchetto
    print("\nStruttura del pacchetto:")
    packet.show()
    
    # Visualizzazione esadecimale
    print("\nRappresentazione esadecimale:")
    hexdump(packet)
    
    # FASE 2: Invio e ricezione
    print(f"\n[FASE 2] Invio pacchetti e ascolto risposte...")
    print(f"Timeout: 3 secondi")
    print(f"Intervallo tra pacchetti: 0.1 secondi\n")
    
    try:
        answered, unanswered = srp(
            packet,
            timeout=3,
            verbose=1,
            inter=0.1
        )
    except PermissionError:
        print("\n[ERRORE] Sono richiesti privilegi di amministratore!")
        print("Esegui lo script con: sudo python3 script.py")
        sys.exit(1)
    except Exception as e:
        print(f"\n[ERRORE] Si è verificato un errore: {e}")
        sys.exit(1)
    
    # FASE 3: Analisi risultati
    print(f"\n{'=' * 70}")
    print("[FASE 3] RISULTATI SCANSIONE")
    print(f"{'=' * 70}\n")
    
    print(f"Host che hanno risposto: {len(answered)}")
    print(f"Host non raggiungibili: {len(unanswered)}")
    
    if answered:
        print(f"\n{'IP ADDRESS':<20} {'MAC ADDRESS':<20} {'STATUS'}")
        print("-" * 60)
        
        for sent, received in answered:
            print(f"{received.psrc:<20} {received.hwsrc:<20} ATTIVO")
            
            # Analisi dettagliata del primo host trovato
            if received == answered[0][1]:
                print(f"\n{'=' * 70}")
                print("ANALISI DETTAGLIATA PRIMO HOST")
                print(f"{'=' * 70}")
                print("\nPacchetto ricevuto:")
                received.show()
                print("\nDati grezzi (hexdump):")
                hexdump(received)
    else:
        print("\nNessun host ha risposto alla scansione!")
    
    return answered, unanswered


def analyze_single_host(ip_address):
    """
    Analizza un singolo host specifico
    
    Args:
        ip_address (str): Indirizzo IP dell'host da analizzare
    """
    
    print(f"\n{'=' * 70}")
    print(f"ANALISI HOST: {ip_address}")
    print(f"{'=' * 70}\n")
    
    # Costruzione pacchetto per singolo host
    packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=ip_address)
    
    # Invio con possibilità di risposte multiple
    answered, unanswered = srp(
        packet,
        timeout=2,
        verbose=0,
        multi=True  # Permette di rilevare IP duplicati
    )
    
    if not answered:
        print(f"L'host {ip_address} non ha risposto")
        return
    
    if len(answered) > 1:
        print(f"ATTENZIONE: Rilevati {len(answered)} dispositivi con lo stesso IP!")
        print("Possibile conflitto di indirizzi IP\n")
    
    for idx, (sent, received) in enumerate(answered, 1):
        print(f"Risposta #{idx}:")
        print(f"  IP: {received.psrc}")
        print(f"  MAC: {received.hwsrc}")
        print(f"  Tempo di risposta: {received.time - sent.sent_time:.6f}s")
        print()


def main():
    """
    Funzione principale
    """
    
    # Configurazione
    network_range = "192.168.1.0/24"  # Modifica secondo la tua rete
    
    print("""
╔═══════════════════════════════════════════════════════════════╗
║          SCAPY NETWORK DISCOVERY & ANALYSIS TOOL              ║
║                                                               ║
║  Questo script dimostra l'uso di:                            ║
║  - Ether: Costruzione frame Ethernet                         ║
║  - ARP: Address Resolution Protocol                          ║
║  - srp: Send and Receive Packets (Layer 2)                   ║
║  - hexdump: Visualizzazione esadecimale                      ║
╚═══════════════════════════════════════════════════════════════╝
    """)
    
    # Esegui scansione completa
    answered, unanswered = discover_network(network_range)
    
    # Se ci sono host attivi, analizza il primo in dettaglio
    if answered:
        first_host_ip = answered[0][1].psrc
        input(f"\nPremi INVIO per analizzare in dettaglio l'host {first_host_ip}...")
        analyze_single_host(first_host_ip)
    
    print(f"\n{'=' * 70}")
    print("SCANSIONE COMPLETATA")
    print(f"{'=' * 70}\n")


if __name__ == "__main__":
    main()

6. Considerazioni Etiche e Legali

Uso Responsabile di Scapy

Scapy è uno strumento estremamente potente che permette di manipolare il traffico di rete a basso livello. Con questo potere viene la responsabilità di usarlo eticamente e legalmente.

Quando è Legale Usare Scapy

Usi legittimi:

Quando è Illegale Usare Scapy

Usi illegali:

Raccomandazioni per Studenti e Professionisti

  1. Ambiente di test isolato: Usa sempre laboratori virtuali (VirtualBox, VMware, GNS3) per sperimentare
  2. Autorizzazione scritta: Ottieni sempre permesso scritto prima di testare reti aziendali
  3. Documentazione: Mantieni traccia di tutte le attività di testing
  4. Scope limitato: Rimani entro i confini concordati durante penetration test
  5. Responsabilità disclosure: Segnala le vulnerabilità trovate in modo responsabile

7. Esercizi Pratici

Esercizio 1: Basic ARP Discovery

Obiettivo: Scoprire tutti i dispositivi nella tua rete locale

# Completa il codice
from scapy.all import Ether, ARP, srp

# TODO: Definisci il range IP della tua rete
target = "___.___.___.___ /___"

# TODO: Costruisci il pacchetto ARP
packet = ___

# TODO: Invia e ricevi
answered, unanswered = ___

# TODO: Stampa i risultati
for ___, ___ in answered:
    print(f"IP: ___ - MAC: ___")

Esercizio 2: Analisi Hexdump

Obiettivo: Creare un pacchetto e analizzare la sua struttura esadecimale

from scapy.all import Ether, ARP, hexdump

# TODO: Crea un pacchetto ARP request per 192.168.1.1
packet = ___

# TODO: Visualizza il pacchetto in formato strutturato
___

# TODO: Visualizza il pacchetto in formato esadecimale
___

# TODO: Identifica manualmente nel hexdump:
# - Dove inizia l'indirizzo MAC di destinazione?
# - Dove si trova il campo 'operation' di ARP?
# - Dove è codificato l'indirizzo IP 192.168.1.1?

Esercizio 3: Rilevamento Host Duplicati

Obiettivo: Implementare un sistema di rilevamento conflitti IP

from scapy.all import Ether, ARP, srp

def find_duplicate_ips(network_range):
    """
    TODO: Implementa una funzione che:
    1. Scansiona il network_range
    2. Identifica IP usati da più dispositivi
    3. Stampa un report dei conflitti trovati
    """
    pass

# Test
find_duplicate_ips("192.168.1.0/24")

Esercizio 4: Network Mapper con Statistiche

Obiettivo: Creare uno script che genera statistiche di rete

from scapy.all import Ether, ARP, srp
import time

def network_statistics(network_range):
    """
    TODO: Implementa uno script che calcola:
    - Tempo medio di risposta degli host
    - Percentuale di host attivi vs totali possibili
    - Lista vendor dei MAC address (ricerca online)
    - Grafico della distribuzione IP
    """
    pass

8. Troubleshooting e FAQ

Problema: “Operation not permitted”

Sintomo: Errore quando si cerca di inviare pacchetti

Soluzione:

# Linux/Mac
sudo python3 script.py

# Windows
# Esegui il terminale come amministratore

Problema: “No module named ‘scapy’”

Sintomo: Python non trova il modulo Scapy

Soluzione:

pip install scapy
# oppure
pip3 install scapy

Problema: Timeout sempre vuoto (nessun host trovato)

Possibili cause:

  1. Firewall sta bloccando i pacchetti ARP
  2. Interfaccia di rete sbagliata
  3. Range IP errato

Soluzione:

from scapy.all import get_if_list, conf

# Verifica interfacce disponibili
print(get_if_list())

# Specifica l'interfaccia corretta
srp(packet, iface="eth0", timeout=3)

Problema: “Sniffing on … but no packet received”

Causa: Interfaccia di rete in modalità promiscua non supportata o virtualizata

Soluzione: Verifica le impostazioni della scheda di rete virtuale se stai usando VM


9. Risorse Aggiuntive

Documentazione Ufficiale

Libri Consigliati

Tutorial Online

Strumenti Complementari


10. Conclusioni

In questa guida abbiamo esplorato in profondità quattro componenti fondamentali di Scapy:

  1. Ether: La base per lavorare con frame Ethernet e comunicazioni di livello 2
  2. ARP: Il protocollo essenziale per la risoluzione IP-MAC nelle reti locali
  3. hexdump: Lo strumento per visualizzare e comprendere i pacchetti a livello binario
  4. srp: La funzione che rende possibile l’interazione bidirezionale con la rete

Padroneggiare questi concetti ti permette di:

Scapy è uno strumento che cresce con te: più lo usi, più scopri le sue potenzialità. Inizia con esempi semplici, sperimenta in ambienti sicuri, e gradualmente costruisci script sempre più complessi.

Ricorda: con grande potere viene grande responsabilità. Usa sempre Scapy in modo etico e legale.


Appendice A: Quick Reference

Import Base

from scapy.all import Ether, ARP, hexdump, srp

Creare Frame Ethernet

# Broadcast
ether = Ether(dst="ff:ff:ff:ff:ff:ff")

# Unicast
ether = Ether(dst="aa:bb:cc:dd:ee:ff", src="11:22:33:44:55:66")

Creare Pacchetti ARP

# ARP Request
arp = ARP(pdst="192.168.1.1")

# ARP Reply
arp = ARP(op=2, hwsrc="aa:bb:cc:dd:ee:ff", psrc="192.168.1.100")

Inviare e Ricevere

# Send and Receive (Layer 2)
answered, unanswered = srp(packet, timeout=2, verbose=0)

Visualizzare Pacchetti

# Vista strutturata
packet.show()

# Vista esadecimale
hexdump(packet)

Iterare sui Risultati

for sent, received in answered:
    print(f"IP: {received.psrc}, MAC: {received.hwsrc}")

Versione: 1.0
Ultima modifica: Gennaio 2025
Autore: Guida didattica per studenti ITI e Universitari
Licenza: Materiale educativo - Uso libero per scopi didattici